{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 7,
   "source": [
    "from IPython.core.interactiveshell import InteractiveShell\r\n",
    "InteractiveShell.ast_node_interactivity = \"all\""
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Flask 中的认证\r\n",
    "\r\n",
    "认证是 web 应用程序最关键和最重要的方面之一。它防止未经授权的人留在一个网站的保护区。如果你对 cookies 有很好的理解，并且知道如何正确地散列密码，你可以推出你自己的身份验证系统。这可能是一个有趣的小项目，以测试你的技能。\r\n",
    "\r\n",
    "正如你可能已经猜到的那样，已经有一个扩展可以让你的生活更加轻松。烧瓶-登录是一个扩展，允许您集成认证系统到您的烧瓶应用程序容易。使用以下命令安装 `Flask-Login` 及其依赖项:\r\n",
    "\r\n",
    "`pip install flask-login`\r\n",
    "\r\n",
    "## 创建用户模型\r\n",
    "\r\n",
    "目前，我们没有存储任何关于将成为我们网站管理员/发布者的用户的数据。因此，我们的第一个任务是创建一个用户模型来存储用户数据。打开 main2.py 文件，在 Employee 模型下面添加 User 模型，如下所示:\r\n",
    "\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#..\r\n",
    "class User(db.Model):\r\n",
    "    __tablename__ = 'users'\r\n",
    "    id = db.Column(db.Integer(), primary_key=True)\r\n",
    "    name = db.Column(db.String(100))\r\n",
    "    username = db.Column(db.String(50), nullable=False, unique=True)\r\n",
    "    email = db.Column(db.String(100), nullable=False, unique=True)\r\n",
    "    password_hash = db.Column(db.String(100), nullable=False)\r\n",
    "    created_on = db.Column(db.DateTime(), default=datetime.utcnow)\r\n",
    "    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)\r\n",
    "\r\n",
    "    def __repr__(self):\r\n",
    "        return \"<{}:{}>\".format(self.id, self.username)\r\n",
    "#...\r\n",
    "```\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\r\n",
    "为了更新数据库，我们需要创建一个新的迁移。在终端输入以下命令来创建迁移脚本:\r\n",
    "\r\n",
    "`python main2.py db migrate -m \"Adding users table\"`\r\n",
    "\r\n",
    "使用 upgrade 命令运行迁移，如下所示:\r\n",
    "\r\n",
    "`python main2.py db upgrade`\r\n",
    "\r\n",
    "这将在数据库中创建用户表。\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "source": [
    "%run main2.py db migrate -m \"Adding users table\"\r\n",
    "%run main2.py db upgrade"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stderr",
     "text": [
      "INFO  [alembic.runtime.migration] Context impl MySQLImpl.\n",
      "INFO  [alembic.runtime.migration] Will assume non-transactional DDL.\n"
     ]
    },
    {
     "output_type": "error",
     "ename": "SystemExit",
     "evalue": "1",
     "traceback": [
      "An exception has occurred, use %tb to see the full traceback.\n",
      "\u001b[1;31mSystemExit\u001b[0m\u001b[1;31m:\u001b[0m 1\n"
     ]
    },
    {
     "output_type": "stream",
     "name": "stderr",
     "text": [
      "INFO  [alembic.runtime.migration] Context impl MySQLImpl.\n",
      "INFO  [alembic.runtime.migration] Will assume non-transactional DDL.\n"
     ]
    },
    {
     "output_type": "error",
     "ename": "SystemExit",
     "evalue": "1",
     "traceback": [
      "An exception has occurred, use %tb to see the full traceback.\n",
      "\u001b[1;31mSystemExit\u001b[0m\u001b[1;31m:\u001b[0m 1\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "source": [
    "# 在Jupyter Notebook中运行SQL\r\n",
    "# 载入sql命令环境\r\n",
    "%load_ext sql\r\n",
    "%sql mysql+pymysql://root:flask123@localhost/flask_app_db"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "source": [
    "%sql  select *  from users"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      " * mysql+pymysql://root:***@localhost/flask_app_db\n",
      "(pymysql.err.ProgrammingError) (1146, \"Table 'flask_app_db.users' doesn't exist\")\n",
      "[SQL: select * from users]\n",
      "(Background on this error at: http://sqlalche.me/e/13/f405)\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\r\n",
    "## 哈希密码\r\n",
    "\r\n",
    "决不能在数据库中将用户密码以纯文本形式存储。万一有恶意用户侵入你的数据库，他就能够读取所有与之相关的密码和电子邮件。众所周知，大多数人在多个网站上使用相同的密码，这意味着攻击者也可以访问用户的其他在线帐户。\r\n",
    "\r\n",
    "与直接将密码存储在数据库中不同，我们将存储密码散列。散列只是一个看起来随机的长字符串，如下所示:\r\n",
    "\r\n",
    "`pbkdf2:sha256:50000$Otfe3YgZ$4fc9f1d2de2b6beb0b888278f21a8c0777e8ff980016e043f3eacea9f48f6dea`\r\n",
    "\r\n",
    "使用单向散列函数创建散列。单向哈希函数接受一个可变长度的输入，并返回一个固定长度的输出，我们称之为哈希。使它安全的是，一旦我们有了散列，我们就不能返回生成它的原始字符串(因此是`单向的`)。对于相同的输入，单向散列函数将始终返回相同的结果。\r\n",
    "\r\n",
    "下面是使用密码散列时所涉及的工作流程:\r\n",
    "\r\n",
    "当用户给出他们的密码时(在注册阶段) ，对其进行散列(hash计算)，然后将散列(hash计算结果)保存到数据库中。当用户登录时，根据输入的密码创建散列(hash计算)，然后将其与存储在数据库中的散列(原hash计算结果)进行比较。如果它们匹配，则登录用户。否则，显示错误消息。\r\n",
    "\r\n",
    "Flask 附带了一个名为 Werkzeug 的包，它为密码散列提供了以下两个辅助函数。\r\n",
    "\r\n",
    "| Method      方法                                   | Description   描述                                               |      |\r\n",
    "| :---------- | :----------------------------------------------------------- |:-------------------------------------------------- |\r\n",
    "| `generate_password_hash(password)`             | It accepts a password and returns a hash. By default, it uses pbkdf2 one-way function to generate the hash. |      |\r\n",
    "| `check_password_hash(password_hash, password)` | It accepts password hash and password in plain text, then compares the hash of `password` with the `password_hash`. If both are same, it returns `True`, otherwise `False`. |      |\r\n",
    "| `generate_password_hash(password)`             | 它接受一个密码并返回一个散列，默认情况下，它使用 pbkdf2单向函数来生成散列 |      |\r\n",
    "| `check_password_hash(password_hash, password)` | 它接受纯文本形式的密码散列和密码，然后将密码的哈希值与存储在数据库中的 `password_hash` 列进行比较。如果两者相同，它返回 `True`，否则`False`. |      |"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "下面的 shell 会话展示了如何使用这些函数:"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "source": [
    "from werkzeug.security import generate_password_hash, check_password_hash\r\n",
    "\r\n",
    "hash = generate_password_hash(\"secret password\")\r\n",
    "\r\n",
    "hash\r\n",
    "\r\n",
    "check_password_hash(hash, \"secret password\")\r\n",
    "\r\n",
    "check_password_hash(hash, \"pass\")"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "'pbkdf2:sha256:150000$RScW3mAC$42ae3804cafaed7c5a485f1035a80a0e618c0f6cfd6c223954a323e0868993b1'"
      ]
     },
     "metadata": {},
     "execution_count": 3
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "metadata": {},
     "execution_count": 3
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "metadata": {},
     "execution_count": 3
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "注意，当使用正确的密码(`\"secret password\"`)调用 `check_password_hash()`时，它返回 `True` ，而使用错误的密码(pass)调用时，它返回 False。\r\n",
    "\r\n",
    "接下来，更新 User 模型以实现密码散列，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from werkzeug.security import generate_password_hash, check_password_hash\r\n",
    "#...\r\n",
    "\r\n",
    "#...\r\n",
    "class User(db.Model):\r\n",
    "    #...\r\n",
    "    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)\r\n",
    "\r\n",
    "    def __repr__(self):\r\n",
    "        return \"<{}:{}>\".format(self.id, self.username)\r\n",
    "\r\n",
    "    def set_password(self, password):\r\n",
    "        self.password_hash = generate_password_hash(password)\r\n",
    "\r\n",
    "    def check_password(self, password):\r\n",
    "        return check_password_hash(self.password_hash, password)        \r\n",
    "    #...\r\n",
    "```    \r\n",
    "\r\n",
    "让我们创建一些用户，并对密码哈希进行测试。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "from main2 import db, User\r\n",
    "\r\n",
    "u1 = User(username='spike', email='spike@example.com')\r\n",
    "u1.set_password(\"spike\")\r\n",
    "\r\n",
    "u2 = User(username='tyke', email='tyke@example.com')\r\n",
    "u2.set_password(\"tyke\")\r\n",
    "\r\n",
    "db.session.add_all([u1, u2])\r\n",
    "db.session.commit()\r\n"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "source": [
    "\r\n",
    "u1.check_password(\"pass\")\r\n",
    "\r\n",
    "u1.check_password(\"spike\")\r\n",
    "\r\n",
    "u2.check_password(\"foo\")\r\n",
    "\r\n",
    "u2.check_password(\"tyke\")\r\n"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "metadata": {},
     "execution_count": 9
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "metadata": {},
     "execution_count": 9
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "metadata": {},
     "execution_count": 9
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "metadata": {},
     "execution_count": 9
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "如输出所示，一切正常工作，现在我们的数据库中有两个用户。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 集成Flask-Login\r\n",
    "\r\n",
    "要 初始化 Flask-Login, 需要从 `Flask-Login` 包导入 `LoginManager` 类,  并创建一个新的 `LoginManager` 实例，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from werkzeug.security import generate_password_hash, check_password_hash\r\n",
    "from flask_login import LoginManager\r\n",
    "\r\n",
    "app = Flask(__name__)\r\n",
    "app.debug = True\r\n",
    "app.config['SECRET_KEY'] = 'a really really really really long secret key'\r\n",
    "app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'\r\n",
    "app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False\r\n",
    "app.config['MAIL_SERVER'] = 'smtp.googlemail.com'\r\n",
    "app.config['MAIL_PORT'] = 587\r\n",
    "app.config['MAIL_USE_TLS'] = True\r\n",
    "app.config['MAIL_USERNAME'] = 'infooveriq@gmail.com'\r\n",
    "app.config['MAIL_DEFAULT_SENDER'] = 'infooveriq@gmail.com'\r\n",
    "app.config['MAIL_PASSWORD'] = 'password'\r\n",
    "\r\n",
    "manager = Manager(app)\r\n",
    "manager.add_command('db', MigrateCommand)\r\n",
    "db = SQLAlchemy(app)\r\n",
    "migrate = Migrate(app, db)\r\n",
    "mail = Mail(app)\r\n",
    "login_manager = LoginManager(app)\r\n",
    "#...\r\n",
    "```\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "要对用户进行身份验证，Flask-Login 需要在 User 类中实现一些特殊方法。下表列出所需的方法:\r\n",
    "\r\n",
    "| 方法                 | 描述                                                      |\r\n",
    "| :------------------- | :-------------------------------------------------------- |\r\n",
    "| `is_authenticated()` | 回报 `True` 如果用户已通过身份验证(即登录) ，否则`False`. |\r\n",
    "| `is_active()`        | 回报 `True` 如果帐户没有被 停用，否则`False`.               |\r\n",
    "| `is_anonymous()`     | 回报 `True` 用于匿名用户(即未登录的用户) ，否则`False`.     |\r\n",
    "| `get_id()`           | 返回唯一标识符 对象                                       |\r\n",
    "\r\n",
    "Flask-Login 还通过 `UserMixin` 类提供这些方法的默认实现。因此，我们可以直接从 `UserMixin` 类继承这些方法，而不必手动定义所有这些方法。打开 main2.py 并修改 User 模型头，如下所示:\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from flask_login import LoginManager, UserMixin  #KEY LINE\r\n",
    "\r\n",
    "#...\r\n",
    "class User(db.Model, UserMixin):   #KEY LINE  多重继承, 从 `UserMixin` 类继承这些方法，而不必手动定义所有这些方法\r\n",
    "    __tablename__ = 'users'\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "剩下的唯一事情就是添加一个 user_loader 回调。\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "@login_manager.user_loader \r\n",
    "def load_user(user_id):\r\n",
    "    return db.session.query(User).get(user_id)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "用 `user_loader` 装饰器 修饰过的函数将在每次请求到达服务器时被调用。它从会话 cookie 中存储的用户 id 加载用户。FLASK 允许用户通过 `current_user` 代理来加载用户信息。要使用 `current_user` ，请从 `flask_login` 包中导入它。它就像一个全局变量，可以在视图函数和模板中使用。在任何时候，`current_user` 要么指向已登录的用户，要么指向一个匿名用户。我们可以使用 `current_user` 的 `is_authenticated` 属性来区分这两个用户类型。对于匿名用户，`is_authenticated` 属性返回 False，否则为 True。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 限制对视图的访问\r\n",
    "\r\n",
    "就目前情况而言，我们没有任何管理区域在我们的网站。对于这一课，管理区域将由一个虚拟页面表示。为了防止未经授权的用户访问受保护的页面 Flask-Login 提供了一个名为 login_required 的装饰器。在 main2.py 中，在 `updating_session()` 视图函数下面添加以下代码:\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from flask_login import LoginManager, UserMixin, login_required\r\n",
    "#...\r\n",
    "@app.route('/admin/')\r\n",
    "@login_required\r\n",
    "def admin():\r\n",
    "    return render_template('admin.html')\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "`login_required` 装饰器 确保只有在用户已经登录时才调用 admin ()视图函数。默认情况下，当匿名用户(未登录的用户)试图访问受保护视图时，将显示 HTTP 401 未授权的页面。\r\n",
    "\r\n",
    "启动服务器，如果还没有运行的话，访问 http://localhost:5000/admin/ ，你会看到这样一个页面:\r\n",
    "\r\n",
    "`Unauthorized `\r\n",
    "\r\n",
    "与其显示401未经授权的错误，不如将用户重定向到登录页面。为了实现这一点，将 LoginManager 实例的 `login_view` 属性设置为 `login` 视图函数，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "```python\r\n",
    "#...\r\n",
    "migrate = Migrate(app, db)\r\n",
    "mail = Mail(app)\r\n",
    "login_manager = LoginManager(app)\r\n",
    "login_manager.login_view = 'login'   # KEY LINE\r\n",
    "\r\n",
    "class Faker(Command):\r\n",
    "    'A command to add fake data to the tables'\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "目前，login ()函数的定义如下(我们很快将对其进行更改) :\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "@app.route('/login/', methods=['post', 'get'])\r\n",
    "def login():\r\n",
    "    message = ''\r\n",
    "    if request.method == 'POST':\r\n",
    "        print(request.form)\r\n",
    "        username = request.form.get('username')\r\n",
    "        password = request.form.get('password')\r\n",
    "\r\n",
    "        if username == 'root' and password == 'pass':\r\n",
    "            message = \"Correct username and password\"\r\n",
    "        else:\r\n",
    "            message = \"Wrong username or password\"\r\n",
    "\r\n",
    "    return render_template('login.html', message=message)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "现在访问 http://localhost:5000/admin/ ，你会被重定向到登录页面:\r\n",
    "\r\n",
    "当用户被重定向到登录页面时，Flask-Login 也会设置 flash 消息，但我们没有看到任何消息，因为登录模板 (template/login.html) 没有显示任何 flash 消息。 打开 login.html 并在 <form> 标记之前添加以下代码，如下所示（突出显示更改）：\r\n",
    "\r\n",
    "Flask_app/templates/login.html\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "    {% endif %}\r\n",
    "\r\n",
    "    {% for category, message in get_flashed_messages(with_categories=true) %}\r\n",
    "        <spam class=\"{{ category }}\">{{ message }}</spam>\r\n",
    "    {% endfor %}\r\n",
    "    \r\n",
    "    <form action=\"\" method=\"post\">\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "再次访问 http://localhost:5000/admin/ ，这次你会在登录页面上看到如下的flash message:\r\n",
    "\r\n",
    "`Please log in to access page.`\r\n",
    "\r\n",
    "要更改 flash 消息，只需向 LoginManager 实例的 login_message 属性分配一条新消息。\r\n",
    "\r\n",
    "`login_manager.login_message = '请先登录或获取授权访问本页面'   # KEY LINE `\r\n",
    "\r\n",
    "在这里，让我们创建一个 `admin()` 视图函数使用的模板。使用以下代码创建一个新的模板名称 `admin.html` :\r\n",
    "\r\n",
    "flask_app/templates/admin.html\r\n",
    "\r\n",
    "```html\r\n",
    "<!DOCTYPE html>\r\n",
    "<html lang=\"en\">\r\n",
    "<head>\r\n",
    "    <meta charset=\"UTF-8\">\r\n",
    "    <title>Title</title>\r\n",
    "</head>\r\n",
    "<body>\r\n",
    "\r\n",
    "<h2>Logged in User details</h2>\r\n",
    "\r\n",
    "<ul>\r\n",
    "    <li>Username: {{ current_user.username }}</li>\r\n",
    "    <li>Email: {{ current_user.email }}</li>\r\n",
    "    <li>Created on: {{ current_user.created_on }}</li>\r\n",
    "    <li>Updated on: {{ current_user.updated_on }}</li>\r\n",
    "</ul>\r\n",
    "\r\n",
    "</body>\r\n",
    "</html>\r\n",
    "```\r\n",
    "\r\n",
    "这里我们使用 current_user 变量来打印登录用户的详细信息。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 创建登录表单\r\n",
    "\r\n",
    "在登录之前，我们需要一个登录表单。登录表单有三个字段: 用户名、密码和记住我。打开 `forms.py` 并在 `ContactForm` 类下面添加 `LoginForm` 类，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/forms.py\r\n",
    "```python\r\n",
    "#...\r\n",
    "from wtforms import StringField, SubmitField, TextAreaField, BooleanField, PasswordField # KEY LINE\r\n",
    "#...\r\n",
    "#...\r\n",
    "class LoginForm(FlaskForm):\r\n",
    "    username = StringField(\"Username\", validators=[DataRequired()])\r\n",
    "    password = PasswordField(\"Password\", validators=[DataRequired()])\r\n",
    "    remember = BooleanField(\"Remember Me\")\r\n",
    "    submit = SubmitField()\r\n",
    "```\r\n",
    "\r\n",
    "## 登录用户\r\n",
    "\r\n",
    "Flask-Login 扩展库提供了 `login_user()` 函数。它接受要登录的用户对象。成功时，它返回 True 并建立一个会话。否则，返回 False。默认情况下，`当浏览器关闭时`，由 login_user() 建立的会话过期。要让用户长时间保持登录状态，请记住在登录用户的同时传一个 `remember=True` 参数给 `login_user()` 函数，。打开 main2.py 并修改 login ()视图函数如下(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#... 全程高亮\r\n",
    "from forms import ContactForm, LoginForm\r\n",
    "#...\r\n",
    "from flask_login import LoginManager, UserMixin, login_required, login_user, current_user\r\n",
    "\r\n",
    "#...\r\n",
    "@app.route('/login/', methods=['post', 'get'])\r\n",
    "def login():\r\n",
    "    form = LoginForm()\r\n",
    "    if form.validate_on_submit():\r\n",
    "        user = db.session.query(User).filter(User.username == form.username.data).first()\r\n",
    "        if user and user.check_password(form.password.data):\r\n",
    "            login_user(user, remember=form.remember.data)  # remember=True ?\r\n",
    "            return redirect(url_for('admin'))\r\n",
    "\r\n",
    "        flash(\"Invalid username/password\", 'error')\r\n",
    "        return redirect(url_for('login'))\r\n",
    "    return render_template('login.html', form=form)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "接下来，我们需要更新 login.html 来使用 LoginForm ()类:\r\n",
    "\r\n",
    "flask_app/templates/login.html\r\n",
    "\r\n",
    "```html\r\n",
    "\r\n",
    "<!DOCTYPE html>\r\n",
    "<html lang=\"en\">\r\n",
    "<head>\r\n",
    "    <meta charset=\"UTF-8\">\r\n",
    "    <title>Login</title>\r\n",
    "</head>\r\n",
    "<body>\r\n",
    "\r\n",
    "    {% for category, message in get_flashed_messages(with_categories=true) %}\r\n",
    "        <spam class=\"{{ category }}\">{{ message }}</spam>\r\n",
    "    {% endfor %}\r\n",
    "\r\n",
    "    <form action=\"\" method=\"post\">\r\n",
    "        {{ form.csrf_token }}\r\n",
    "        <p>\r\n",
    "            {{ form.username.label() }}\r\n",
    "            {{ form.username() }}\r\n",
    "            {% if form.username.errors %}\r\n",
    "                {% for error in form.username.errors %}\r\n",
    "                    {{ error }}\r\n",
    "                {% endfor %}\r\n",
    "            {% endif %}\r\n",
    "        </p>\r\n",
    "        <p>\r\n",
    "            {{ form.password.label() }}\r\n",
    "            {{ form.password() }}\r\n",
    "            {% if form.password.errors %}\r\n",
    "                {% for error in form.password.errors %}\r\n",
    "                    {{ error }}\r\n",
    "                {% endfor %}\r\n",
    "            {% endif %}\r\n",
    "        </p>\r\n",
    "        <p>\r\n",
    "            {{ form.remember.label() }}\r\n",
    "            {{ form.remember() }}\r\n",
    "        </p>\r\n",
    "        <p>\r\n",
    "            {{ form.submit() }}\r\n",
    "        </p>\r\n",
    "    </form>\r\n",
    "\r\n",
    "</body>\r\n",
    "</html>\r\n",
    "\r\n",
    "```\r\n",
    "\r\n",
    "我们现在已经准备好登录了，访问 google http://localhost:5000/admin ，你将被重定向到登录页面。\r\n",
    "\r\n",
    "![img](updated_login_page-a6ac2956-3ce9-4c83-ba4e-2285b3817fea.png)\r\n",
    "输入正确的用户名和密码并点击提交。你将被重定向到管理页面，应该是这样的:\r\n",
    "\r\n",
    "![img](admin_page-1de4701c-1bf9-4bf0-b24f-1c2b4bad5ecb.png)\r\n",
    "\r\n",
    "如果你在登录时没有选中“ Remember Me”复选框，那么一旦浏览器关闭，你就会被注销。否则，您将保持登录状态。\r\n",
    "\r\n",
    "当输入无效的用户名或密码时，你会被重定向到登录页面，并且会看到一条闪光信息，如下图所示:\r\n",
    "\r\n",
    "![img](https://overiq.com/media/uploads/2018/1/19/invalid_username_and_password_flash_message-45827559-e538-4d5b-9f66-d5b2768d519a.png)\r\n",
    "\r\n",
    "\r\n",
    "##  注销用户\r\n",
    "\r\n",
    "Flask 的 logout_user() 函数通过删除会话中存储的用户 id 来注销用户。在 main2.py 文件中，在 login() 视图函数下面添加以下代码:\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "#...\r\n",
    "from flask_login import LoginManager, UserMixin, login_required, login_user, current_user, logout_user\r\n",
    "#...\r\n",
    "@app.route('/logout/')\r\n",
    "@login_required\r\n",
    "def logout():\r\n",
    "    logout_user()    \r\n",
    "    flash(\"You have been logged out.\")\r\n",
    "    return redirect(url_for('login'))\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "接下来，更新 admin. html 模板，使其包含一个注销路由链接，如下所示(突出显示更改) :\r\n",
    "\r\n",
    "flask_app/templates/admin.html\r\n",
    "```html\r\n",
    "#...\r\n",
    "<ul>\r\n",
    "    <li>Username: {{ current_user.username }}</li>\r\n",
    "    <li>Email: {{ current_user.email }}</li>\r\n",
    "    <li>Created on: {{ current_user.created_on }}</li>\r\n",
    "    <li>Updated on: {{ current_user.updated_on }}</li>\r\n",
    "</ul>\r\n",
    "\r\n",
    "<p><a href=\"{{ url_for('logout') }}\">Logout</a></p>\r\n",
    "\r\n",
    "</body>\r\n",
    "</html>\r\n",
    "```\r\n",
    "\r\n",
    "如果你现在访问 http://localhost:5000/admin/ (假设你已经登录) ，你会在页面底部看到一个注销链接。\r\n",
    "\r\n",
    "![img](https://overiq.com/media/uploads/2018/1/19/logout_link_in_admin_page-72f429cb-e792-4074-a9bc-ab9015246a0c.png)\r\n",
    "\r\n",
    "若要注销，请单击链接，您将被重定向到登录页面。\r\n",
    "\r\n",
    "![img](https://overiq.com/media/uploads/2018/1/19/logout_flash_message-5ecc1a45-c5cf-41e7-b246-f80ae7f6f82a.png)\r\n",
    "\r\n",
    "## 最后的完善\r\n",
    "\r\n",
    "登录页面还有一个小问题。现在，如果一个登录用户访问 http://localhost:5000/login/ ，他会再次看到登录页面。向已经登录的用户显示登录表单是没有意义的。要解决此问题，请在 login ()视图函数中进行以下更改。\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "```python\r\n",
    "#...\r\n",
    "@app.route('/login/', methods=['post', 'get'])\r\n",
    "def login():\r\n",
    "    if current_user.is_authenticated:   # KEY LINE\r\n",
    "        return redirect(url_for('admin'))  # KEY LINE\r\n",
    "    form = LoginForm()\r\n",
    "    if form.validate_on_submit():\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "在这些更改之后，如果登录用户访问了登录页面，他将被重定向到管理页面。\r\n",
    "\r\n"
   ],
   "metadata": {}
  }
 ],
 "metadata": {
  "orig_nbformat": 4,
  "language_info": {
   "name": "python",
   "version": "3.9.6",
   "mimetype": "text/x-python",
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "pygments_lexer": "ipython3",
   "nbconvert_exporter": "python",
   "file_extension": ".py"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3.9.6 64-bit"
  },
  "interpreter": {
   "hash": "c644d696b95f5e0f4df3c6556741cf30bcf9ea6ca93c3e1f29fcf31d885534fc"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}